#!/usr/bin/env node
var fs          = require('fs');
var path        = require('path');
var when        = require('when');
var utils       = require('./utils');


exports.Dictionary = ( function() {

    var Entry = ( function() {

        function Entry(entryName) {
            this.variants = {}
            this.missing = 0;
            this.entryName = entryName;
        }

        Entry.prototype.insert = function(variant) {
            if (variant.type in this.variants) {
                this.variants[variant.type].insert(variant);
            } else {
                this.variants[variant.type] = variant;
            }
        }

        Entry.prototype.finish = function() {
            if (!this.json) {
                this.json = {
                    variants : {}
                };
                for (var variantName in this.variants) {
                    var variant = this.variants[variantName];
                    this.json.variants[variantName] = variant.finish();
                    this.missing+=variant.missing;
                }
                this.json.hits = this.hits();
            }
            return this.json;
        }

        Entry.prototype.hits = function() {
            var hits = 0;
            for (var variantName in this.variants) {
                if (hits < this.variants[variantName].hits) {
                    hits = this.variants[variantName].hits;
                }
            }
            return hits;
        }

        return Entry;
    })();


    var Variant = ( function() {

        function Variant(parentDictionary, entryName, type, properties, hits) {
            this.parentDictionary = parentDictionary;
            this.type = type;
            this.hits = hits;
            this.missing = 0;
            this.entryName = entryName;
            this.entryProperties = properties;
            if (this.parentDictionary.templateSpecs[type]) {
                this.tree = JSON.parse(JSON.stringify(this.parentDictionary.templateSpecs[type]));
            }
        }

        Variant.prototype.process = function() {
            if (this.entryProperties.english) {
                this.english = this.entryProperties.english;
                if (!this.parentDictionary.out[this.entryName]) {
                    this.parentDictionary.out[this.entryName] = new Entry(this.entryName);
                }
                this.parentDictionary.out[this.entryName].insert(this);
            } else if (this.entryProperties.meta) {
                if (processMeta(this, this.entryProperties.meta, this.tree, this.entryName)) {
                    //template matches, let's see if we can reparent it (find a root entry for it)
                    if (!this.findAndMergeWithRoot()) {
                        this.parentDictionary.orphans.push(this);
                    }
                }
            } else {
                throw new Error("Unable to process entry '"+JSON.stringify(this.entryProperties)+"', no meta");
            }
        }

        Variant.prototype.insert = function(object) {
            if (object.english) {
                this.english = this.english.concat(object.english);
            } else if (object.entryProperties.meta.length) {
                insert(this.tree, object.tree);
            }
            this.hits += object.hits;
        }

        Variant.prototype.findAndMergeWithRoot = function() {
            if (!this.entryProperties.meta.length) {
                throw new Error("Unable to findAndMergeWithRoot for '"+JSON.stringify(this.entryProperties)+"', nothing left in meta");
            } else if (this.english) {
                throw new Error("Unable to findAndMergeWithRoot for '"+JSON.stringify(this.entryProperties)+"', already root for '"+this.english+"'");
            } else {
                for (var m = 0; m < this.entryProperties.meta.length; m++) {
                    var metaItem = this.entryProperties.meta[m];
                    if (metaItem in this.parentDictionary.out) {
                        this.parentDictionary.out[metaItem].variants[this.type].insert(this);
                        this.mergeError = undefined;
                        return true;
                    }
                }
            }
            return false;
        }


        Variant.prototype.finish = function() {
            var result = this.tree ? toJSON(this, this.tree) : {};
            result.english = this.english;
            return result;
        }

        function toJSON(self, node) {
            var result = node;
            var nodeType = typeof node;
            if ( nodeType === "object") {
                result = {};
                var subNodesNo = 0;
                for (var memberName in node) {
                    if (memberName === "tags") {
                        continue;
                    }
                    subNodesNo++;
                    var tempResult = toJSON(self, node[memberName]);
                    if (JSON.stringify(tempResult) !== JSON.stringify({}) ) {
                        result[memberName] = tempResult;
                    }
                }
                if (!subNodesNo) {
                    self.missing++;
                }
            }
            return result;
        }

        function insert(dstNode, srcNode) {
            var srcType = typeof srcNode;
            if (srcType === "object") {
                for (var srcNodeName in srcNode) {
                    var result = insert(dstNode[srcNodeName], srcNode[srcNodeName]);
                    if (result) {
                        dstNode[srcNodeName] = result;
                    }
                }
            } else if (srcType === "string") {
                return srcNode;
            } else {
                throw new Error("Unexpected type in template expansion for node: '"+srcNode+"'");
            }
        }

        function meta2TagMatch(meta, tags) {
            for (var t = 0; t < tags.reject; t++) {
                var tag = tag.rejects[t];
                for (var m = 0; m < meta.length; m++) {
                    var metaItem = meta[m];
                    if (metaItem === tag) {
                        //found
                        return false;
                    }
                }
            }

            var found = false;
            for (var t = 0; t < tags.require.length; t++) {
                var tag = tags.require[t];
                for (var m = 0; m < meta.length; m++) {
                    var metaItem = meta[m];
                    if (metaItem === tag) {
                        //found
                        meta.splice(m, 1);
                        found = true;
                    }
                }
            }
            return found;
        }

        function processMeta(self, meta, node, entryName) {
            var hasMembers = false;
            var foundNode = entryName;
            for (var templateMemberName in node) {
                if (templateMemberName === 'tags') {
                    delete node[templateMemberName];
                    continue;
                }
                if (!hasMembers) {
                    foundNode = undefined;
                    hasMembers = true;
                }
                var templateMember = node[templateMemberName];
                if (foundNode) {
                    delete node[templateMemberName];
                    continue;
                } else if (typeof templateMember === "object") {
                    if (!Array.isArray(templateMember)) {
                        if (!templateMember.tags || meta2TagMatch(meta, templateMember.tags)) {
                            node[templateMemberName] = processMeta(self, meta, templateMember, entryName);
                            foundNode = node;
                        }
                    } else {
                        for (var i = 0; i < templateMember.length; i++) {
                            templateMember[i] = processMeta(self, meta, templateMember[i], entryName);
                            if (!templateMember[i]) {
                                templateMember.splice(i,1);
                            }
                        }
                        if (templateMember.length) {
                            foundNode = node;
                        }
                    }
                }
                if (!foundNode) {
                    delete node[templateMemberName];
                }
            }
            return foundNode;
        }

        return Variant;
    })();

    function Dictionary(templateSpec) {
        this.spec = templateSpec;
        this.templateSpecs = inflate(this);
        this.out = {};
        this.orphans = [];
        this.dependencies  = [module.filename];
        this.failures = 0;
        this.missing = 0;
    }

    Dictionary.prototype.process = function(entryName, entryProperties) {
        for (var type in entryProperties.variants) {
            for (var i = 0; i < entryProperties.variants[type].length; i++) {
                var wordObject = new Variant(this, entryName, type, entryProperties.variants[type][i], entryProperties.hits ? entryProperties.hits : 0);
                try {
                    wordObject.process();
                } catch (e) {
                    this.failures++;
                }
            }
        }
    }

    Dictionary.prototype.finish = function(forEachFunction) {
        for (var i = 0; i < this.orphans.length; i++) {
            try {
                if (this.orphans[i].findAndMergeWithRoot()) {
                    this.orphans.splice(i,1);
                    i--;
                }
            } catch (e) {}
        }

        var outArray = [];
        for (var entryName in this.out) {
            var entry = this.out[entryName];
            outArray.push({
                entry : entryName,
                properties : entry.finish()
            });
            this.missing+=entry.missing;
            forEachFunction();
        }
        return outArray;
    }

    function inflate(self) {
        var result = {};
        for (var node in self.spec.nodes) {
            result[node] = inflateNode(self, self.spec.nodes[node]);
        }
        return result;
    }

    function inflateNode(self, node) {
        var result;
        var nodeType = typeof node;
        switch(nodeType) {
            case "object":
                if (Array.isArray(node)) {
                    result = [];
                    for (var i = 0; i < node.length; i++) {
                        result.push(inflateNode(self, node[i]));
                    }
                } else {
                    result = {};
                    for (var memberName in node) {
                        var member = node[memberName];
                        if (memberName === "class") {
                            //instantiate a class
                            if (JSON.stringify(result) !== "{}") {
                                throw new Exception("Illegal class reference in node: '"+JSON.stringify(node)+"'");
                            }
                            result = inflateNode(self, JSON.parse(JSON.stringify(self.spec.classes[member])));
                            break;
                        } else if (memberName === "cases") {
                            //we'll process that later
                            continue;
                        }
                        result[memberName] = inflateNode(self, member);
                    }

                    if (node.cases) {
                        var caseTemplate = JSON.parse(JSON.stringify(result));
                        result = {};
                        for (var caseName in node.cases) {
                            result[caseName] = {};

                            //compose case instance
                            // - copy the nodes declared for every case
                            var caseInstance = JSON.parse(JSON.stringify(caseTemplate));
                            // - add the nodes declared for this case
                            var caseItem = node.cases[caseName];
                            for (var caseItemName in caseItem) {
                                caseInstance[caseItemName] = caseItem[caseItemName];
                            }

                            //inflate case
                            for (var caseItemName in caseInstance) {
                                result[caseName][caseItemName] = inflateNode(self, caseInstance[caseItemName]);
                            }

                            //if tags weren't defined, assume the default
                            if (!result[caseName].tags) {
                                result[caseName].tags = {
                                    require : [caseName]
                                };
                            }
                        }
                    }
                }
            break;
            default:
                result = node;
        }
        return result;
    }

    return Dictionary;
})();
